/*UNIHIKER K10 Light Meter by mircemk, August 2025 */ #include #include #include "Adafruit_LTR329_LTR303.h" #include "unihiker_k10.h" #define M_SIZE 1.3333 TFT_eSPI tft = TFT_eSPI(); #define TFT_GREY 0x5AEB #define TFT_LIGHTPINK 0xFDB8 #define TFT_GOLD 0xFEA0 #define TFT_LIGHTGREEN 0x9772 #define TFT_LIGHTSALMON 0xFD0F #define INFO_PANEL_HEIGHT M_SIZE*30 #define FOOTER_HEIGHT M_SIZE*20 // Height for the version footer UNIHIKER_K10 k10; uint8_t screen_dir=2; // Light sensor Adafruit_LTR303 ltr; // Meter variables float ltx = 0; uint16_t osx = M_SIZE*120, osy = M_SIZE*120; uint32_t updateTime = 0; int old_analog = -999; // Light sensor variables uint16_t minLight = 0; uint16_t maxLight = 1000; uint32_t lastSensorRead = 0; const uint32_t SENSOR_READ_INTERVAL = 100; void setup(void) { Serial.begin(115200); k10.begin(); k10.initScreen(screen_dir); if (!ltr.begin()) { Serial.println("Couldn't find LTR sensor!"); while (1) delay(10); } ltr.setGain(LTR3XX_GAIN_1); ltr.setIntegrationTime(LTR3XX_INTEGTIME_50); ltr.setMeasurementRate(LTR3XX_MEASRATE_50); tft.init(); tft.setRotation(1); tft.fillScreen(TFT_BLACK); analogMeter(); drawInfoPanel(0); drawFooter(); updateTime = millis(); } void loop() { uint16_t visible_plus_ir, infrared; bool valid; if (millis() - lastSensorRead > SENSOR_READ_INTERVAL) { lastSensorRead = millis(); if (ltr.newDataAvailable()) { valid = ltr.readBothChannels(visible_plus_ir, infrared); if (valid) { if (visible_plus_ir < minLight) minLight = visible_plus_ir; if (visible_plus_ir > maxLight) maxLight = visible_plus_ir; int mappedValue = map(visible_plus_ir, minLight, maxLight, 0, 100); mappedValue = constrain(mappedValue, 0, 100); plotNeedle(mappedValue, 10); drawInfoPanel(visible_plus_ir); } } } } void drawInfoPanel(uint16_t lightValue) { // Clear and draw panel tft.fillRect(0, M_SIZE*126, M_SIZE*239, INFO_PANEL_HEIGHT, TFT_YELLOW); tft.drawRect(0, M_SIZE*126, M_SIZE*239, INFO_PANEL_HEIGHT, TFT_BLACK); // Set larger font (Font 4) tft.setTextColor(TFT_BLACK, TFT_YELLOW); tft.setTextFont(4); // Larger font // Format and display text char infoText[40]; snprintf(infoText, sizeof(infoText), "RAW:%-5d RNG:%d-%d", lightValue, minLight, maxLight); // Center the text in the panel int16_t textWidth = tft.textWidth(infoText, 4); int16_t xPos = (M_SIZE*239 - textWidth) / 2; tft.setCursor(xPos, M_SIZE*133); tft.print(infoText); } void drawFooter() { // Sky blue footer tft.fillRect(0, M_SIZE*126 + INFO_PANEL_HEIGHT, M_SIZE*239, FOOTER_HEIGHT, TFT_CYAN); // Centered version text tft.setTextColor(TFT_BLACK, TFT_CYAN); tft.setTextFont(2); // Medium-sized font const char* versionText = "Light Intensity Meter V1.0 by mircemk"; int16_t textWidth = tft.textWidth(versionText, 2); int16_t xPos = (M_SIZE*239 - textWidth) / 2; int16_t yPos = M_SIZE*126 + INFO_PANEL_HEIGHT + (FOOTER_HEIGHT/2 - 8); // Vertically centered tft.setCursor(xPos, yPos); tft.print(versionText); } // ######################################################################### // Draw the analogue meter on the screen // ######################################################################### void analogMeter() { // Meter outline tft.fillRect(0, 0, M_SIZE*239, M_SIZE*126, TFT_LIGHTSALMON); tft.fillRect(5, 3, M_SIZE*230, M_SIZE*119, TFT_WHITE); tft.setTextColor(TFT_BLACK); // Text colour // Draw ticks every 5 degrees from -50 to +50 degrees (100 deg. FSD swing) for (int i = -50; i < 51; i += 5) { // Long scale tick length int tl = 15; // Coodinates of tick to draw float sx = cos((i - 90) * 0.0174532925); float sy = sin((i - 90) * 0.0174532925); uint16_t x0 = sx * (M_SIZE*100 + tl) + M_SIZE*120; uint16_t y0 = sy * (M_SIZE*100 + tl) + M_SIZE*140; uint16_t x1 = sx * M_SIZE*100 + M_SIZE*120; uint16_t y1 = sy * M_SIZE*100 + M_SIZE*140; // Coordinates of next tick for zone fill float sx2 = cos((i + 5 - 90) * 0.0174532925); float sy2 = sin((i + 5 - 90) * 0.0174532925); int x2 = sx2 * (M_SIZE*100 + tl) + M_SIZE*120; int y2 = sy2 * (M_SIZE*100 + tl) + M_SIZE*140; int x3 = sx2 * M_SIZE*100 + M_SIZE*120; int y3 = sy2 * M_SIZE*100 + M_SIZE*140; // Green zone limits if (i >= 0 && i < 25) { tft.fillTriangle(x0, y0, x1, y1, x2, y2, TFT_GREEN); tft.fillTriangle(x1, y1, x2, y2, x3, y3, TFT_GREEN); } // Orange zone limits if (i >= 25 && i < 50) { tft.fillTriangle(x0, y0, x1, y1, x2, y2, TFT_ORANGE); tft.fillTriangle(x1, y1, x2, y2, x3, y3, TFT_ORANGE); } // Short scale tick length if (i % 25 != 0) tl = 8; // Recalculate coords incase tick lenght changed x0 = sx * (M_SIZE*100 + tl) + M_SIZE*120; y0 = sy * (M_SIZE*100 + tl) + M_SIZE*140; x1 = sx * M_SIZE*100 + M_SIZE*120; y1 = sy * M_SIZE*100 + M_SIZE*140; // Draw tick tft.drawLine(x0, y0, x1, y1, TFT_BLACK); // Check if labels should be drawn, with position tweaks if (i % 25 == 0) { // Calculate label positions x0 = sx * (M_SIZE*100 + tl + 10) + M_SIZE*120; y0 = sy * (M_SIZE*100 + tl + 10) + M_SIZE*140; switch (i / 25) { case -2: tft.drawCentreString("0", x0, y0 - 12, 2); break; case -1: tft.drawCentreString("25", x0, y0 - 9, 2); break; case 0: tft.drawCentreString("50", x0, y0 - 7, 2); break; case 1: tft.drawCentreString("75", x0, y0 - 9, 2); break; case 2: tft.drawCentreString("100", x0, y0 - 12, 2); break; } } // Now draw the arc of the scale sx = cos((i + 5 - 90) * 0.0174532925); sy = sin((i + 5 - 90) * 0.0174532925); x0 = sx * M_SIZE*100 + M_SIZE*120; y0 = sy * M_SIZE*100 + M_SIZE*140; // Draw scale arc, don't draw the last part if (i < 50) tft.drawLine(x0, y0, x1, y1, TFT_BLACK); } tft.drawString("Light", M_SIZE*(5 + 230 - 40), M_SIZE*(119 - 20), 2); // Units at bottom right tft.drawCentreString("Light", M_SIZE*120, M_SIZE*70, 4); // Comment out to avoid font 4 tft.drawRect(5, 3, M_SIZE*230, M_SIZE*119, TFT_BLACK); // Draw bezel line plotNeedle(0, 0); // Put meter needle at 0 } // ######################################################################### // Update needle position // ######################################################################### void plotNeedle(int value, byte ms_delay) { tft.setTextColor(TFT_BLACK, TFT_WHITE); char buf[8]; dtostrf(value, 4, 0, buf); tft.drawRightString(buf, M_SIZE*40, M_SIZE*(119 - 20), 2); if (value < -10) value = -10; // Limit value to emulate needle end stops if (value > 110) value = 110; // Move the needle until new value reached while (!(value == old_analog)) { if (old_analog < value) old_analog++; else old_analog--; if (ms_delay == 0) old_analog = value; // Update immediately if delay is 0 float sdeg = map(old_analog, -10, 110, -150, -30); // Map value to angle // Calcualte tip of needle coords float sx = cos(sdeg * 0.0174532925); float sy = sin(sdeg * 0.0174532925); // Calculate x delta of needle start (does not start at pivot point) float tx = tan((sdeg + 90) * 0.0174532925); // Erase old needle image tft.drawLine(M_SIZE*(120 + 20 * ltx - 1), M_SIZE*(140 - 20), osx - 1, osy, TFT_WHITE); tft.drawLine(M_SIZE*(120 + 20 * ltx), M_SIZE*(140 - 20), osx, osy, TFT_WHITE); tft.drawLine(M_SIZE*(120 + 20 * ltx + 1), M_SIZE*(140 - 20), osx + 1, osy, TFT_WHITE); // Re-plot text under needle tft.setTextColor(TFT_BLACK); tft.drawCentreString("Light", M_SIZE*120, M_SIZE*70, 4); // Store new needle end coords for next erase ltx = tx; osx = M_SIZE*(sx * 98 + 120); osy = M_SIZE*(sy * 98 + 140); // Draw the needle in the new postion tft.drawLine(M_SIZE*(120 + 20 * ltx - 1), M_SIZE*(140 - 20), osx - 1, osy, TFT_RED); tft.drawLine(M_SIZE*(120 + 20 * ltx), M_SIZE*(140 - 20), osx, osy, TFT_MAGENTA); tft.drawLine(M_SIZE*(120 + 20 * ltx + 1), M_SIZE*(140 - 20), osx + 1, osy, TFT_RED); // Slow needle down slightly as it approaches new postion if (abs(old_analog - value) < 10) ms_delay += ms_delay / 5; // Wait before next update delay(ms_delay); } }